Skip to main content
Glama

Edit-MCP

unix.rs20.3 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. //! Unix-specific platform code. //! //! Read the `windows` module for reference. //! TODO: This reminds me that the sys API should probably be a trait. use std::ffi::{CStr, c_int, c_void}; use std::fs::{self, File}; use std::mem::{self, ManuallyDrop, MaybeUninit}; use std::os::fd::{AsRawFd as _, FromRawFd as _}; use std::path::Path; use std::ptr::{self, NonNull, null_mut}; use std::{thread, time}; use crate::arena::{Arena, ArenaString, scratch_arena}; use crate::helpers::*; use crate::{apperr, arena_format}; #[cfg(target_os = "netbsd")] const fn desired_mprotect(flags: c_int) -> c_int { // NetBSD allows an mmap(2) caller to specify what protection flags they // will use later via mprotect. It does not allow a caller to move from // PROT_NONE to PROT_READ | PROT_WRITE. // // see PROT_MPROTECT in man 2 mmap flags << 3 } #[cfg(not(target_os = "netbsd"))] const fn desired_mprotect(_: c_int) -> c_int { libc::PROT_NONE } struct State { stdin: libc::c_int, stdin_flags: libc::c_int, stdout: libc::c_int, stdout_initial_termios: Option<libc::termios>, inject_resize: bool, // Buffer for incomplete UTF-8 sequences (max 4 bytes needed) utf8_buf: [u8; 4], utf8_len: usize, } static mut STATE: State = State { stdin: libc::STDIN_FILENO, stdin_flags: 0, stdout: libc::STDOUT_FILENO, stdout_initial_termios: None, inject_resize: false, utf8_buf: [0; 4], utf8_len: 0, }; extern "C" fn sigwinch_handler(_: libc::c_int) { unsafe { STATE.inject_resize = true; } } pub fn init() -> apperr::Result<Deinit> { unsafe { // Reopen stdin if it's redirected (= piped input). if libc::isatty(STATE.stdin) == 0 { STATE.stdin = check_int_return(libc::open(c"/dev/tty".as_ptr(), libc::O_RDONLY))?; } // Store the stdin flags so we can more easily toggle `O_NONBLOCK` later on. STATE.stdin_flags = check_int_return(libc::fcntl(STATE.stdin, libc::F_GETFL))?; Ok(Deinit) } } pub struct Deinit; impl Drop for Deinit { fn drop(&mut self) { unsafe { #[allow(static_mut_refs)] if let Some(termios) = STATE.stdout_initial_termios.take() { // Restore the original terminal modes. libc::tcsetattr(STATE.stdout, libc::TCSANOW, &termios); } } } } pub fn switch_modes() -> apperr::Result<()> { unsafe { // Set STATE.inject_resize to true whenever we get a SIGWINCH. let mut sigwinch_action: libc::sigaction = mem::zeroed(); sigwinch_action.sa_sigaction = sigwinch_handler as libc::sighandler_t; check_int_return(libc::sigaction(libc::SIGWINCH, &sigwinch_action, null_mut()))?; // Get the original terminal modes so we can disable raw mode on exit. let mut termios = MaybeUninit::<libc::termios>::uninit(); check_int_return(libc::tcgetattr(STATE.stdin, termios.as_mut_ptr()))?; let mut termios = termios.assume_init(); STATE.stdout_initial_termios = Some(termios); termios.c_iflag &= !( // When neither IGNBRK... libc::IGNBRK // ...nor BRKINT are set, a BREAK reads as a null byte ('\0'), ... | libc::BRKINT // ...except when PARMRK is set, in which case it reads as the sequence \377 \0 \0. | libc::PARMRK // Disable input parity checking. | libc::INPCK // Disable stripping of eighth bit. | libc::ISTRIP // Disable mapping of NL to CR on input. | libc::INLCR // Disable ignoring CR on input. | libc::IGNCR // Disable mapping of CR to NL on input. | libc::ICRNL // Disable software flow control. | libc::IXON ); // Disable output processing. termios.c_oflag &= !libc::OPOST; termios.c_cflag &= !( // Reset character size mask. libc::CSIZE // Disable parity generation. | libc::PARENB ); // Set character size back to 8 bits. termios.c_cflag |= libc::CS8; termios.c_lflag &= !( // Disable signal generation (SIGINT, SIGTSTP, SIGQUIT). libc::ISIG // Disable canonical mode (line buffering). | libc::ICANON // Disable echoing of input characters. | libc::ECHO // Disable echoing of NL. | libc::ECHONL // Disable extended input processing (e.g. Ctrl-V). | libc::IEXTEN ); // Set the terminal to raw mode. termios.c_lflag &= !(libc::ICANON | libc::ECHO); check_int_return(libc::tcsetattr(STATE.stdin, libc::TCSANOW, &termios))?; Ok(()) } } pub fn inject_window_size_into_stdin() { unsafe { STATE.inject_resize = true; } } fn get_window_size() -> (u16, u16) { let mut winsz: libc::winsize = unsafe { mem::zeroed() }; for attempt in 1.. { let ret = unsafe { libc::ioctl(STATE.stdout, libc::TIOCGWINSZ, &raw mut winsz) }; if ret == -1 || (winsz.ws_col != 0 && winsz.ws_row != 0) { break; } if attempt == 10 { winsz.ws_col = 80; winsz.ws_row = 24; break; } // Some terminals are bad emulators and don't report TIOCGWINSZ immediately. thread::sleep(time::Duration::from_millis(10 * attempt)); } (winsz.ws_col, winsz.ws_row) } /// Reads from stdin. /// /// Returns `None` if there was an error reading from stdin. /// Returns `Some("")` if the given timeout was reached. /// Otherwise, it returns the read, non-empty string. pub fn read_stdin(arena: &Arena, mut timeout: time::Duration) -> Option<ArenaString<'_>> { unsafe { if STATE.inject_resize { timeout = time::Duration::ZERO; } let read_poll = timeout != time::Duration::MAX; let mut buf = Vec::new_in(arena); // We don't know if the input is valid UTF8, so we first use a Vec and then // later turn it into UTF8 using `from_utf8_lossy_owned`. // It is important that we allocate the buffer with an explicit capacity, // because we later use `spare_capacity_mut` to access it. buf.reserve(4 * KIBI); // We got some leftover broken UTF8 from a previous read? Prepend it. if STATE.utf8_len != 0 { STATE.utf8_len = 0; buf.extend_from_slice(&STATE.utf8_buf[..STATE.utf8_len]); } loop { if timeout != time::Duration::MAX { let beg = time::Instant::now(); let mut pollfd = libc::pollfd { fd: STATE.stdin, events: libc::POLLIN, revents: 0 }; let ret; #[cfg(target_os = "linux")] { let ts = libc::timespec { tv_sec: timeout.as_secs() as libc::time_t, tv_nsec: timeout.subsec_nanos() as libc::c_long, }; ret = libc::ppoll(&mut pollfd, 1, &ts, ptr::null()); } #[cfg(not(target_os = "linux"))] { ret = libc::poll(&mut pollfd, 1, timeout.as_millis() as libc::c_int); } if ret < 0 { return None; // Error? Let's assume it's an EOF. } if ret == 0 { break; // Timeout? We can stop reading. } timeout = timeout.saturating_sub(beg.elapsed()); }; // If we're asked for a non-blocking read we need // to manipulate `O_NONBLOCK` and vice versa. set_tty_nonblocking(read_poll); // Read from stdin. let spare = buf.spare_capacity_mut(); let ret = libc::read(STATE.stdin, spare.as_mut_ptr() as *mut _, spare.len()); if ret > 0 { buf.set_len(buf.len() + ret as usize); break; } if ret == 0 { return None; // EOF } if ret < 0 { match errno() { libc::EINTR if STATE.inject_resize => break, libc::EAGAIN if timeout == time::Duration::ZERO => break, libc::EINTR | libc::EAGAIN => {} _ => return None, } } } if !buf.is_empty() { // We only need to check the last 3 bytes for UTF-8 continuation bytes, // because we should be able to assume that any 4 byte sequence is complete. let lim = buf.len().saturating_sub(3); let mut off = buf.len() - 1; // Find the start of the last potentially incomplete UTF-8 sequence. while off > lim && buf[off] & 0b1100_0000 == 0b1000_0000 { off -= 1; } let seq_len = match buf[off] { b if b & 0b1000_0000 == 0 => 1, b if b & 0b1110_0000 == 0b1100_0000 => 2, b if b & 0b1111_0000 == 0b1110_0000 => 3, b if b & 0b1111_1000 == 0b1111_0000 => 4, // If the lead byte we found isn't actually one, we don't cache it. // `from_utf8_lossy_owned` will replace it with U+FFFD. _ => 0, }; // Cache incomplete sequence if any. if off + seq_len > buf.len() { STATE.utf8_len = buf.len() - off; STATE.utf8_buf[..STATE.utf8_len].copy_from_slice(&buf[off..]); buf.truncate(off); } } let mut result = ArenaString::from_utf8_lossy_owned(buf); // We received a SIGWINCH? Add a fake window size sequence for our input parser. // I prepend it so that on startup, the TUI system gets first initialized with a size. if STATE.inject_resize { STATE.inject_resize = false; let (w, h) = get_window_size(); if w > 0 && h > 0 { let scratch = scratch_arena(Some(arena)); let seq = arena_format!(&scratch, "\x1b[8;{h};{w}t"); result.replace_range(0..0, &seq); } } result.shrink_to_fit(); Some(result) } } pub fn write_stdout(text: &str) { if text.is_empty() { return; } // If we don't set the TTY to blocking mode, // the write will potentially fail with EAGAIN. set_tty_nonblocking(false); let buf = text.as_bytes(); let mut written = 0; while written < buf.len() { let w = &buf[written..]; let w = &buf[..w.len().min(GIBI)]; let n = unsafe { libc::write(STATE.stdout, w.as_ptr() as *const _, w.len()) }; if n >= 0 { written += n as usize; continue; } let err = errno(); if err != libc::EINTR { return; } } } /// Sets/Resets `O_NONBLOCK` on the TTY handle. /// /// Note that setting this flag applies to both stdin and stdout, because the /// TTY is a bidirectional device and both handles refer to the same thing. fn set_tty_nonblocking(nonblock: bool) { unsafe { let is_nonblock = (STATE.stdin_flags & libc::O_NONBLOCK) != 0; if is_nonblock != nonblock { STATE.stdin_flags ^= libc::O_NONBLOCK; let _ = libc::fcntl(STATE.stdin, libc::F_SETFL, STATE.stdin_flags); } } } pub fn open_stdin_if_redirected() -> Option<File> { unsafe { // Did we reopen stdin during `init()`? if STATE.stdin != libc::STDIN_FILENO { Some(File::from_raw_fd(libc::STDIN_FILENO)) } else { None } } } #[derive(Clone, PartialEq, Eq)] pub struct FileId { st_dev: libc::dev_t, st_ino: libc::ino_t, } /// Returns a unique identifier for the given file by handle or path. pub fn file_id(file: Option<&File>, path: &Path) -> apperr::Result<FileId> { let file = match file { Some(f) => f, None => &File::open(path)?, }; unsafe { let mut stat = MaybeUninit::<libc::stat>::uninit(); check_int_return(libc::fstat(file.as_raw_fd(), stat.as_mut_ptr()))?; let stat = stat.assume_init(); Ok(FileId { st_dev: stat.st_dev, st_ino: stat.st_ino }) } } /// Reserves a virtual memory region of the given size. /// To commit the memory, use `virtual_commit`. /// To release the memory, use `virtual_release`. /// /// # Safety /// /// This function is unsafe because it uses raw pointers. /// Don't forget to release the memory when you're done with it or you'll leak it. pub unsafe fn virtual_reserve(size: usize) -> apperr::Result<NonNull<u8>> { unsafe { let ptr = libc::mmap( null_mut(), size, desired_mprotect(libc::PROT_READ | libc::PROT_WRITE), libc::MAP_PRIVATE | libc::MAP_ANONYMOUS, -1, 0, ); if ptr.is_null() || ptr::eq(ptr, libc::MAP_FAILED) { Err(errno_to_apperr(libc::ENOMEM)) } else { Ok(NonNull::new_unchecked(ptr as *mut u8)) } } } /// Releases a virtual memory region of the given size. /// /// # Safety /// /// This function is unsafe because it uses raw pointers. /// Make sure to only pass pointers acquired from `virtual_reserve`. pub unsafe fn virtual_release(base: NonNull<u8>, size: usize) { unsafe { libc::munmap(base.cast().as_ptr(), size); } } /// Commits a virtual memory region of the given size. /// /// # Safety /// /// This function is unsafe because it uses raw pointers. /// Make sure to only pass pointers acquired from `virtual_reserve` /// and to pass a size less than or equal to the size passed to `virtual_reserve`. pub unsafe fn virtual_commit(base: NonNull<u8>, size: usize) -> apperr::Result<()> { unsafe { let status = libc::mprotect(base.cast().as_ptr(), size, libc::PROT_READ | libc::PROT_WRITE); if status != 0 { Err(errno_to_apperr(libc::ENOMEM)) } else { Ok(()) } } } unsafe fn load_library(name: &CStr) -> apperr::Result<NonNull<c_void>> { unsafe { NonNull::new(libc::dlopen(name.as_ptr(), libc::RTLD_LAZY)) .ok_or_else(|| errno_to_apperr(libc::ENOENT)) } } /// Loads a function from a dynamic library. /// /// # Safety /// /// This function is highly unsafe as it requires you to know the exact type /// of the function you're loading. No type checks whatsoever are performed. // // It'd be nice to constrain T to std::marker::FnPtr, but that's unstable. pub unsafe fn get_proc_address<T>(handle: NonNull<c_void>, name: &CStr) -> apperr::Result<T> { unsafe { let sym = libc::dlsym(handle.as_ptr(), name.as_ptr()); if sym.is_null() { Err(errno_to_apperr(libc::ENOENT)) } else { Ok(mem::transmute_copy(&sym)) } } } pub fn load_libicuuc() -> apperr::Result<NonNull<c_void>> { unsafe { load_library(c"libicuuc.so") } } pub fn load_libicui18n() -> apperr::Result<NonNull<c_void>> { unsafe { load_library(c"libicui18n.so") } } /// ICU, by default, adds the major version as a suffix to each exported symbol. /// They also recommend to disable this for system-level installations (`runConfigureICU Linux --disable-renaming`), /// but I found that many (most?) Linux distributions don't do this for some reason. /// This function returns the suffix, if any. #[allow(clippy::not_unsafe_ptr_arg_deref)] pub fn icu_proc_suffix(arena: &Arena, handle: NonNull<c_void>) -> ArenaString<'_> { unsafe { type T = *const c_void; let mut res = ArenaString::new_in(arena); // Check if the ICU library is using unversioned symbols. // Return an empty suffix in that case. if get_proc_address::<T>(handle, c"u_errorName").is_ok() { return res; } // In the versions (63-76) and distributions (Arch/Debian) I tested, // this symbol seems to be always present. This allows us to call `dladdr`. // It's the `UCaseMap::~UCaseMap()` destructor which for some reason isn't // in a namespace. Thank you ICU maintainers for this oversight. let proc = match get_proc_address::<T>(handle, c"_ZN8UCaseMapD1Ev") { Ok(proc) => proc, Err(_) => return res, }; // `dladdr` is specific to GNU's libc unfortunately. let mut info: libc::Dl_info = mem::zeroed(); let ret = libc::dladdr(proc, &mut info); if ret == 0 { return res; } // The library path is in `info.dli_fname`. let path = match CStr::from_ptr(info.dli_fname).to_str() { Ok(name) => name, Err(_) => return res, }; let path = match fs::read_link(path) { Ok(path) => path, Err(_) => path.into(), }; // I'm going to assume it's something like "libicuuc.so.76.1". let path = path.into_os_string(); let path = path.to_string_lossy(); let suffix_start = match path.rfind(".so.") { Some(pos) => pos + 4, None => return res, }; let version = &path[suffix_start..]; let version_end = version.find('.').unwrap_or(version.len()); let version = &version[..version_end]; res.push('_'); res.push_str(version); res } } pub fn add_icu_proc_suffix<'a, 'b, 'r>(arena: &'a Arena, name: &'b CStr, suffix: &str) -> &'r CStr where 'a: 'r, 'b: 'r, { if suffix.is_empty() { name } else { // SAFETY: In this particular case we know that the string // is valid UTF-8, because it comes from icu.rs. let name = unsafe { name.to_str().unwrap_unchecked() }; let mut res = ArenaString::new_in(arena); res.reserve(name.len() + suffix.len() + 1); res.push_str(name); res.push_str(suffix); res.push('\0'); let bytes: &'a [u8] = unsafe { mem::transmute(res.as_bytes()) }; unsafe { CStr::from_bytes_with_nul_unchecked(bytes) } } } pub fn preferred_languages(arena: &Arena) -> Vec<ArenaString<'_>, &Arena> { let mut locales = Vec::new_in(arena); for key in ["LANGUAGE", "LC_ALL", "LANG"] { if let Ok(val) = std::env::var(key) && !val.is_empty() { locales.extend(val.split(':').filter(|s| !s.is_empty()).map(|s| { // Replace all underscores with dashes, // because the localization code expects pt-br, not pt_BR. let mut res = Vec::new_in(arena); res.extend(s.as_bytes().iter().map(|&b| if b == b'_' { b'-' } else { b })); unsafe { ArenaString::from_utf8_unchecked(res) } })); break; } } locales } #[inline] fn errno() -> i32 { // Under `-O -Copt-level=s` the 1.87 compiler fails to fully inline and // remove the raw_os_error() call. This leaves us with the drop() call. // ManuallyDrop fixes that and results in a direct `std::sys::os::errno` call. ManuallyDrop::new(std::io::Error::last_os_error()).raw_os_error().unwrap_or(0) } #[inline] pub(crate) fn io_error_to_apperr(err: std::io::Error) -> apperr::Error { errno_to_apperr(err.raw_os_error().unwrap_or(0)) } pub fn apperr_format(f: &mut std::fmt::Formatter<'_>, code: u32) -> std::fmt::Result { write!(f, "Error {code}")?; unsafe { let ptr = libc::strerror(code as i32); if !ptr.is_null() { let msg = CStr::from_ptr(ptr).to_string_lossy(); write!(f, ": {msg}")?; } } Ok(()) } pub fn apperr_is_not_found(err: apperr::Error) -> bool { err == errno_to_apperr(libc::ENOENT) } const fn errno_to_apperr(no: c_int) -> apperr::Error { apperr::Error::new_sys(if no < 0 { 0 } else { no as u32 }) } fn check_int_return(ret: libc::c_int) -> apperr::Result<libc::c_int> { if ret < 0 { Err(errno_to_apperr(errno())) } else { Ok(ret) } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mixelpixx/microsoft-edit-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server